using System;
using System.Text;
using XTCast.Mp3;
using System.IO;
using System.Collections;
using System.Xml.Serialization;
using System.Runtime.Serialization;

namespace XTCast.Mp3 {

	/// Provides access to ID3 tag metadata supports ID3, ID3v2.2 and ID3v2.3.

	/// <summary>ID3 format version codes </summary>
	public enum ID3Ver {
		None, v1, v22, v23
	}

	[Serializable] public class ID3Tag {

		// Tag names are four-character strings defined in the ID3 specification. 
		// IDs of some of the more common frames.
		public const String kTagTitle = "TIT2";
		public const String kTagArtist = "TPE1";
		public const String kTagBand = "TPE2";
		public const String kTagAlbum = "TALB";
		public const String kTagYear = "TYER";
		public const String kTagGenre = "TCON";
		public const String kTagComment = "COMM";

		private static System.String[] kID3v1Genres = new System.String[] {
			"Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz","Metal",
			"New Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno","Industrial","Alternative",
			"Ska","Death Metal","Pranks","Soundtrack","Euro-Techno","Ambient","Trip-Hop","Vocal","Jazz+Funk",
			"Fusion","Trance","Classical","Instrumental","Acid","House","Game","Sound Clip","Gospel","Noise",
			"AlternRock","Bass","Soul","Punk","Space","Meditative","Instrumental Pop","Instrumental Rock",
			"Ethnic","Gothic","Darkwave","Techno-Industrial","Electronic","Pop-Folk","Eurodance","Dream",
			"Southern Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/Funk","Jungle",
			"Native American","Cabaret","New Wave","Psychadelic","Rave","Showtunes","Trailer","Lo-Fi","Tribal",
			"Acid Punk","Acid Jazz","Polka","Retro","Musical","Rock & Roll","Hard Rock","Folk","Folk-Rock",
			"National Folk","Swing","Fast Fusion","Bebob","Latin","Revival","Celtic","Bluegrass","Avantgarde",
			"Gothic Rock","Progressive Rock","Psychedelic Rock","Symphonic Rock","Slow Rock","Big Band","Chorus",
			"Easy Listening","Acoustic","Humour","Speech","Chanson","Opera","Chamber Music","Sonata","Symphony",
			"Booty Bass","Primus","Porn Groove","Satire","Slow Jam","Club","Tango","Samba","Folklore","Ballad",
			"Power Ballad","Rhythmic Soul","Freestyle","Duet","Punk Rock","Drum Solo","Acapella","Euro-House",
			"Dance Hall","Goa","Drum & Bass","Club-House","Hardcore","Terror","Indie","BritPop","Negerpunk",	
			"Polsk Punk","Beat","Christian","Heavy Metal","Black Metal","Crossover","Contemporary Christian",
			"Christian Rock","Merengue","Salsa","Thrash Metal","Anime","JPop","Synthpop"};

		public String id;
		public short flags;
		public byte[] raw;

		private ID3Ver id3ver;
		private System.String text;

		public ID3Tag(ID3Ver ver) {
			id3ver=ver;
		}

		private string GetGenre(int genreIndex) {
			if (genreIndex>=kID3v1Genres.Length) {
				if (genreIndex!=255) throw new ID3TagException("Unknown Genre #" + genreIndex);
				return "";
			} else
				return kID3v1Genres[genreIndex];
		}

		/// Returns the contents of a textual ID3 tag as a String.
		public virtual string Text {
			get {
				lock(this) {
					if (text == null && raw.Length > 0) {
						// Convert tag data to text:
						if (id3ver == ID3Ver.v1) {
							Encoding encoding = Encoding.GetEncoding("windows-1252");
							
							if (id==kTagGenre && raw.Length==1 ) {
								int genreIndex = (int)raw[0];
								text = GetGenre(genreIndex);
							} else {
								text = encoding.GetString(raw);
							}
						} 
						else {
							// In ID3v2, 1st byte indicates encoding to use
							Encoding encoding = Encoding.GetEncoding((raw[0] == 0)?"ISO-8859-1":"Unicode");
							text = encoding.GetString(raw,1,raw.Length-1).Trim();
							if (id==kTagGenre) {
								int bp = text.IndexOf("(");
								int ep = text.IndexOf(")");
								if (ep>bp && ep-bp<=4) {
									String num=text.Substring(bp+1,ep-bp-1);
									text=text.Remove(bp,ep-bp+1).Trim();
									if (text=="") text = GetGenre(int.Parse(num));
								}
							}
						}
					}
					return text;
				}
			}
			set {
				lock(this) {
					text=value;
					Encoding encoding = Encoding.Unicode;
					raw=new byte[encoding.GetByteCount(text)+1];
					raw[0]=0x01;
					encoding.GetBytes(text,0,text.Length,raw,1);
				}
			}
		}
	}
	/// <summary>
	/// ID3TagCollection holds a collection of ID3Tags
	/// </summary>
	[Serializable] 
	public class ID3TagCollection {

		private String _FullName;
		
		public ID3TagCollection() {
			fTags = new System.Collections.Hashtable();
		}

		public ID3TagCollection(String fname):this() {
			try 
			{
				if (fname==null) return;
				_FullName=fname;
				System.IO.FileStream io = new FileStream(fname,FileMode.Open,FileAccess.Read,FileShare.ReadWrite);
				try 
				{	
					if (!ReadID3v2(new BinaryReader(io),fID3Version))
						if (ReadID3v1(io))
							fID3Version = ID3Ver.v1;
						else
							fID3Version = ID3Ver.None;
				} 
				finally 
				{
					try 
					{
						io.Close();
					}
					catch (System.IO.IOException x) 
					{
						Log.Error(x);
					}
				}
			}
			catch (FileNotFoundException ex) 
			{
				fID3Version = ID3Ver.None;
			}
			catch (Exception ex) 
			{
				fID3Version = ID3Ver.None;
				Log.Error(ex);
			}
		}

		public ID3Ver ID3Version {
			get {
				return fID3Version;
			}
			set {
				fID3Version=value;
			}
		}

		public long EndingOffset {
			get {
				return fOffset;
			}
		}
		
		/// <summary>Attempts to read ID3v1 data; returns false if there isn't any </summary>
		private bool ReadID3v1(System.IO.FileStream io) {
			// Read the last 128 bytes, where the ID3v1 tag lives:
			long start = io.Length - 128;
			if (start <= 0)	return false;

			byte[] buf = new byte[128];
			io.Seek(start, System.IO.SeekOrigin.Begin);
			io.Read(buf,0,buf.Length);
			
			// Check for the ID3v1 header:
			if (buf[0] != 'T' || buf[1] != 'A' || buf[2] != 'G') {
				Log.Warn(this,"No tag found");
				return false;
			}
			
			Log.Verbose(this,"Found ID3v1 tag...");
			
			// Now tweeze out each tag value:
			int pos = 3;
			for (int i = 0; i < kID3v1TagName.Length; i++) {
				int size = kID3v1TagSize[i];
				int len;
				for (len = size; len > 0; len--) {
					char c = (char) buf[pos + len - 1];
					if (c != ' ' && c != '\x0000')
						break;
				}
				if (len > 0) {
					byte[] raw = new byte[len];
					Array.Copy(buf, pos, raw, 0, len);
					ID3Tag tag = new ID3Tag(ID3Ver.v1);
					tag.id = kID3v1TagName[i];
					tag.raw = raw;
					tag.flags = 0;
					fTags[tag.id]=tag;
				}
				pos += size;
			}
			
			return true;
		}

		/// <summary>Attempts to read ID3v2 data; returns false if there isn't any</summary>
		private bool ReadID3v2(System.IO.BinaryReader br, ID3Ver fID3Version) {
			br.BaseStream.Seek(0, System.IO.SeekOrigin.Begin);

			// Check for the ID3v2 header:
			byte[] buf = new byte[3];
			br.Read(buf, 0, buf.Length);

			if (buf[0] != 'I' || buf[1] != 'D' || buf[2] != '3')
				return false;
			
			// Check the ID3v2 version:
			int version = EndianReverse(br.ReadInt16());
			if (version <= 0x02FF)
				fID3Version = ID3Ver.v22;
			else if (version <= 0x03FF)
				fID3Version = ID3Ver.v23;
			else
				throw new ID3TagException("ID3v2 version " + (version / 256.0) + " is not supported");
			
			Log.Verbose(this,"ID3v2 version is " + System.Convert.ToString(version, 16));
			
			// Read the flags:
			int flags;
			bool unsync, extended, experimental;
			flags = br.ReadByte();

			Log.Verbose(this,"ID3v2 flags: 0x" + System.Convert.ToString(flags, 16));
			
			if (fID3Version == ID3Ver.v22) {
				unsync = bit(flags,7);

				if (bit(flags,6)) throw new ID3TagException("Cannot read compressed ID3v2.2 tag");

				extended = false;
			}
			else {
				unsync = bit(flags,7);
				extended = bit(flags,6);
				//experimental = bit(flags,5);
				
				if ((flags & 0x1F) != 0) {
					// Spec says that tag might not be readable if unknown flags are set
					throw new ID3TagException("ID3v2 unknown flags in use: 0x" + System.Convert.ToString(flags, 16));
				}
			}
			
			// Read the total header size:
			int totalSize = decode7BitInt(EndianReverse(br.ReadInt32()));
			
			fOffset = totalSize + 10;
			
			Log.Verbose(this,"TotalSize=" + totalSize);
			
			// Apply unsynchronization if necessary:
			if (unsync) br = new BinaryReader(new UnsyncInputStream(br.BaseStream));
			
			// Read the extended header, if any:
			if (extended) {
				int size = (int)EndianReverse(br.ReadUInt32());
				if (size < 0)
					throw new ID3TagException("Illegal extended header size " + size);
				totalSize -= 4 + size;
				// Just ignore extended header contents
				br.BaseStream.Seek(size, System.IO.SeekOrigin.Current);
			}
			
			// Read frames one at a time:
			byte[] idBuf = new byte[fID3Version == ID3Ver.v22?3:4];
			while (totalSize > 0) {
				br.Read(idBuf, 0, idBuf.Length);
				if (idBuf[0] == 0) {
					// We've reached the end of tags and gone into blank padding space.
					totalSize = 0; // skip totalSize test after end of loop
					break;
				}
				ID3Tag tag = new ID3Tag(fID3Version);
				Encoding enc = Encoding.GetEncoding("ISO-8859-1");
				tag.id = enc.GetString(idBuf);

				if (fID3Version == ID3Ver.v22)
					tag.id = mapID3v22Name(tag.id);
				
				// Read the data size:
				int size;
				if (fID3Version == ID3Ver.v22) {
					// ID3v2.2 field size is 3(!) bytes:
					size = (EndianReverse(br.ReadInt16()) << 16) | ((sbyte) br.ReadByte() & 0xFF);
					totalSize -= 7;
				}
				else {
					size = (int)EndianReverse(br.ReadUInt32());
					tag.flags = EndianReverse(br.ReadInt16());
					totalSize -= 10;
				}
				if (size < 0 || size > totalSize)
					throw new ID3TagException("Illegal frame size " + size + " for frame '" + tag.id + "'");
				totalSize -= size;
				
				// Read the raw data:
				tag.raw = new byte[size];
				br.Read(tag.raw, 0, tag.raw.Length);

				if (tag.id!="GEOB" && tag.id!="NCON" && tag.id!="APIC" && tag.id!="PRIV")
					Log.Verbose(this,tag.id + ":" + tag.Text);
				
				fTags[tag.id]=tag;
			}
			
			if (totalSize != 0)
				throw new ID3TagException("ID3 size mismatch");
			
			return true;
		}
		
		/// <summary>Saves ID3v2 data</summary>
		public void SaveID3v2(System.IO.Stream io) {
			Encoding iso8859 = Encoding.GetEncoding("ISO-8859-1");
			io.Seek(0, System.IO.SeekOrigin.Begin);
			System.IO.BinaryWriter bw = new BinaryWriter(io);
		
			bw.Write(iso8859.GetBytes("ID3"));

			// Write the ID3v2 version:
			bw.Write(EndianReverse((short)0x0300));

			// Write the flags:
			bw.Write((byte)0x80); //Unsync

			int totalSize=1024;
			foreach (DictionaryEntry tagentry in fTags) {
				totalSize+=10+((ID3Tag)tagentry.Value).raw.Length;
			}

			bw.Write((int)EndianReverse(encode7BitInt(totalSize)));
			
			fOffset = totalSize + 10;
			
			long pos=bw.BaseStream.Position;
			bw = new BinaryWriter(new UnsyncInputStream(bw.BaseStream));
			bw.BaseStream.Position=pos;
			
			foreach (DictionaryEntry tagentry in fTags) {

				ID3Tag tag = (ID3Tag)tagentry.Value;

				bw.Write(iso8859.GetBytes(tag.id)); //(4 bytes)
				bw.Write((int)EndianReverse((int)tag.raw.Length)); // 4 bytes
				bw.Write((short)EndianReverse((short)tag.flags)); // 2 bytes
				bw.Write(tag.raw, 0, tag.raw.Length);
				totalSize -= 10 + tag.raw.Length;
			}
			
			while (totalSize > 0) {
				bw.Write(0x00);
				totalSize--;
			}
		}

		public virtual ID3Tag this[string tagName] {
			get {
				return (ID3Tag) fTags[tagName];
			}
			set {
				fTags[tagName]=value;
			}
		}

		private void addTag(ID3Tag tag) {
			fTags[tag.id]=tag;
		}

		/// <summary>Returns true if the file's ID3 data contains the given named tag.
		/// Tag names are four-character strings defined in the ID3 specification.</summary>

		public bool hasTag(System.String name) {
			return fTags.ContainsKey(name);
		}

		public virtual void getTag(System.String name,ref String text) {
			try {
				ID3Tag tag = this[name];
				if (tag != null) text = tag.Text;
			} catch (Exception ex) {
				Log.Error(this,ex);
			}
		}

		public virtual String getTag(System.String name) {
			String value=null;
			getTag(name,ref value);
			return value;
		}

		public virtual void setTag(System.String id, String value) {
			ID3Tag tag = this[id];
			if (tag != null)
				tag.Text = value;
			else {
				tag = new ID3Tag(fID3Version);

				tag.id=id;
				tag.flags=0;
				tag.Text=value;

				this[id]=tag;
			}
		}
		
		/// <summary>Returns the name of the artist who recorded the audio track, as given in the ID3 tag. </summary>		
		public virtual void Update(bool updateID3v1) {
			System.IO.FileStream io = new FileStream(_FullName,FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.Read);

			try {
				SaveID3v2(io);
				//if (saveID3v1) saveID3v1readID3v1(io);
			}
			finally {
				try {
					io.Close();
				}
				catch (System.IO.IOException x) {
				}
			}
		}
		
		/// <summary>Maps an ID3v2.2 frame name to the equivalent ID3v2.3 name. </summary>
		private System.String mapID3v22Name(System.String name) {
			for (int i = 0; i < kID3v22Converter.Length; i += 2)
				if (kID3v22Converter[i].Equals(name))
					return kID3v22Converter[i + 1];
			return name; // by default return it unchanged
		}
		
		// PRIVATE DATA:
		
		/// <summary>Number of bytes into file MPEG audio starts</summary>
		private long fOffset; 
		/// <summary>Version of the ID3 tag.</summary>
		private ID3Ver fID3Version;
		/// <summary>Hashtable mapping ID3v2.3 tag names to Tag objects</summary>
		private System.Collections.Hashtable fTags; 
		
		// PRIVATE CONSTANTS:
		
		private static int[] kID3v1TagSize = new int[] {30, 30, 30, 4, 29, 1, 1};
		private static System.String[] kID3v1TagName = new System.String[]{"TIT2","TPE1","TALB","TYER","COMM","TRCK","TCON"};
		
		/// <summary>Maps ID3v2.2 tag names to their ID3v2.3 equivalents </summary>
		private static System.String[] kID3v22Converter = new System.String[]{"TAL","TALB","TBP","TBPM","TCM","TCOM","TCR","TCOP","TDA","TDAT","TDY","TDLY","TEN","TENC","TXT","TEXT","TFT","TFLT","TIM","TIME","TT1","TIT1","TT2","TIT2","TT3","TIT3","TKE","TKEY","TLA","TLAN","TLE","TLEN","TMT","TMED","TOT","TOAL","TOF","TOFN","TOL","TOLY","TOA","TOPE","TOR","TORY","TP1","TPE1","TP2","TPE2","TP3","TPE3","TP4","TPE4","TPA","TPOS","TPB","TPUB","TRD","TRDA","TSI","TSIZ","TRC","TSRC","TYE","TYER"};
		
		#region "UnsyncStream Private Class"
		/// <summary>
		/// An InputStream that performs the "unsyncing" process that decodes ID3v2 tag data 
		/// </summary>
		private class UnsyncInputStream:Stream {

			private System.IO.Stream baseStream;
			private bool readFF = false;
	
			public UnsyncInputStream(System.IO.Stream input) {
				baseStream = input;
			}
			
			public override int Read(byte[] buffer, int offset, int count) {
				int actual;
				for (actual=0;actual<count;actual++) {
					int i = baseStream.ReadByte();
					if (i == -1) break;
					if (readFF) {
						readFF = false;
						if (i == 0x00) {  // Skip 00 byte after FF [ID3 2.3 spec sect. 5]
							i = baseStream.ReadByte();
							if (i==-1) break;
						}
					}
					else if (i == 0xFF) readFF = true;
					buffer[offset+actual]=(byte)i;
				}
				return actual;
			}

			public Stream BaseStream {
				get {
					return baseStream;
				}
			}

			public override bool CanRead {
				get {
					return baseStream.CanRead;
				}
			}

			public override bool CanSeek {
				get {
					return baseStream.CanSeek;
				}
			}

			public override bool CanWrite {
				get {
					return baseStream.CanWrite;
				}
			}

			public override void Flush() {
				baseStream.Flush();
			}

			public override long Position {
				get {
					return baseStream.Position;
				}
				set {
					baseStream.Position = value;
				}
			}

			public override long Seek(long offset, System.IO.SeekOrigin origin) {
				return baseStream.Seek(offset,origin);
			}

			public override long Length {
				get {
					return baseStream.Length;
				}
			}

			public override void SetLength(long value) {
				baseStream.SetLength(value);
			}

			public override void Write(byte[] buffer, int offset, int count) {
				for (int i=0;i<count;i++) {
					byte b=buffer[offset+i];
					baseStream.WriteByte(b);
					if (b==0xFF) baseStream.WriteByte(0x00);
				}
			}
		}
		#endregion

		private bool bit(int i, int bit) {
			return ((i >> bit) & 1) != 0;
		}

		static uint EndianReverse(uint x) {
			return ((x<<24) | ((x & 0xff00)<<8) | ((x &
				0xff0000)>>8) | (x>>24));
		}

		static int EndianReverse(int x) {
			return ((x<<24) | ((x & 0xff00)<<8) | ((x & 0xff0000)>>8) | (x>>24));
		}

		static ushort EndianReverse(ushort x) {
			return (ushort)((x<<8) | (x>>8));
		}

		static short EndianReverse(short x) {
			return (short)((x<<8) | (x>>8));
		}
				
		/// <summary>Converts an integer from the 7-bit-byte encoding used in ID3v2,
		/// in which the high bit of each byte is unused. </summary>
		private int decode7BitInt(int i) {
			return ((i & 0x0000007F)) | ((i & 0x00007F00) >> 1) | ((i & 0x007F0000) >> 2) | ((i & 0x7F000000) >> 3);
		}

		// <summary> Encodes 28bits of an int into a format where 8th bit is always 0 </summary>
		private int encode7BitInt(int i) {
			return ((i & 0x0000007F)) | ((i & 0x3F80) << 1) | ((i & 0x1FC000) << 2) | ((i & 0xFE00000) << 3);
		}

		override public String ToString() {
			return "IDTags: " + _FullName;
		}

	}

}
